/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.noggit;

import java.io.IOException;
import java.io.StringReader;
import java.lang.invoke.MethodHandles;
import org.apache.solr.SolrTestCaseJ4;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestJSONParser extends SolrTestCaseJ4 {
  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  // these are to aid in debugging if an unexpected error occurs
  static int parserType;
  static int bufferSize;
  static String parserInput;
  static JSONParser lastParser;

  static int flags = JSONParser.FLAGS_DEFAULT; // the default

  public static String lastParser() {
    return "parserType="
        + parserType
        + (parserType == 1 ? " bufferSize=" + bufferSize : "")
        + " parserInput='"
        + parserInput
        + "'"
        + "flags : "
        + lastParser.flags;
  }

  public static JSONParser getParser(String s) {
    return getParser(s, random().nextInt(2), -1);
  }

  public static JSONParser getParser(String s, int type, int bufSize) {
    parserInput = s;
    parserType = type;

    JSONParser parser = null;
    switch (type) {
      case 0:
        // test directly using input buffer
        parser = new JSONParser(s.toCharArray(), 0, s.length());
        break;
      case 1:
        // test using Reader...
        // small input buffers can help find bugs on boundary conditions

        if (bufSize < 1) bufSize = random().nextInt(25) + 1;
        bufferSize = bufSize; // record in case there is an error
        parser = new JSONParser(new StringReader(s), new char[bufSize]);
        break;
    }
    if (parser == null) return null;

    lastParser = parser;

    if (flags != JSONParser.FLAGS_DEFAULT) {
      parser.setFlags(flags);
    }

    return parser;
  }

  // for debugging purposes
  //  public void testSpecific() throws Exception {
  //    JSONParser parser = getParser("[0", 1, 1);
  //    for (; ; ) {
  //      int ev = parser.nextEvent();
  //      if (ev == JSONParser.EOF) {
  //        break;
  //      } else {
  //        System.out.println("got " + JSONParser.getEventString(ev));
  //      }
  //    }
  //  }

  public static byte[] events = new byte[256];

  static {
    events['{'] = JSONParser.OBJECT_START;
    events['}'] = JSONParser.OBJECT_END;
    events['['] = JSONParser.ARRAY_START;
    events[']'] = JSONParser.ARRAY_END;
    events['s'] = JSONParser.STRING;
    events['b'] = JSONParser.BOOLEAN;
    events['l'] = JSONParser.LONG;
    events['n'] = JSONParser.NUMBER;
    events['N'] = JSONParser.BIGNUMBER;
    events['0'] = JSONParser.NULL;
    events['e'] = JSONParser.EOF;
  }

  // match parser states with the expected states
  public static void parse(JSONParser p, String input, String expected) throws IOException {
    expected += "e";
    for (int i = 0; i < expected.length(); i++) {
      int ev = p.nextEvent();
      int expect = events[expected.charAt(i)];
      if (ev != expect) {
        fail(
            "Expected "
                + expect
                + ", got "
                + ev
                + "\n\tINPUT="
                + input
                + "\n\tEXPECTED="
                + expected
                + "\n\tAT="
                + i
                + " ("
                + expected.charAt(i)
                + ")");
      }
    }
  }

  public static void parse(String input, String expected) throws IOException {
    String in = input;
    if ((flags & JSONParser.ALLOW_SINGLE_QUOTES) == 0 || random().nextBoolean()) {
      in = in.replace('\'', '"');
    }

    for (int i = 0; i < Integer.MAX_VALUE; i++) {
      JSONParser p = getParser(in, i, -1);
      if (p == null) break;
      parse(p, in, expected);
    }

    testCorruption(input, 100000);
  }

  public static void testCorruption(String input, int iter) {
    char[] arr = new char[input.length()];

    for (int i = 0; i < iter; i++) {
      input.getChars(0, arr.length, arr, 0);
      int changes = random().nextInt(arr.length >> 1) + 1;
      for (int j = 0; j < changes; j++) {
        char ch;
        switch (random().nextInt(31)) {
          case 0:
            ch = 0;
            break;
          case 1:
            ch = '[';
            break;
          case 2:
            ch = ']';
            break;
          case 3:
            ch = '{';
            break;
          case 4:
            ch = '}';
            break;
          case 5:
            ch = '"';
            break;
          case 6:
            ch = '\'';
            break;
          case 7:
            ch = ' ';
            break;
          case 8:
            ch = '\r';
            break;
          case 9:
            ch = '\n';
            break;
          case 10:
            ch = '\t';
            break;
          case 11:
            ch = ',';
            break;
          case 12:
            ch = ':';
            break;
          case 13:
            ch = '.';
            break;
          case 14:
            ch = 'a';
            break;
          case 15:
            ch = 'e';
            break;
          case 16:
            ch = '0';
            break;
          case 17:
            ch = '1';
            break;
          case 18:
            ch = '+';
            break;
          case 19:
            ch = '-';
            break;
          case 20:
            ch = 't';
            break;
          case 21:
            ch = 'f';
            break;
          case 22:
            ch = 'n';
            break;
          case 23:
            ch = '/';
            break;
          case 24:
            ch = '\\';
            break;
          case 25:
            ch = 'u';
            break;
          case 26:
            ch = '\u00a0';
            break;
          default:
            ch = (char) random().nextInt(256);
        }

        arr[random().nextInt(arr.length)] = ch;
      }

      JSONParser parser = getParser(new String(arr));
      parser.setFlags(random().nextInt()); // set random parser flags

      double ret = 0;
      try {
        for (; ; ) {
          int ev = parser.nextEvent();
          if (random().nextBoolean()) {
            // see if we can read the event
            switch (ev) {
              case JSONParser.STRING:
                ret += parser.getString().length();
                break;
              case JSONParser.BOOLEAN:
                ret += parser.getBoolean() ? 1 : 2;
                break;
              case JSONParser.BIGNUMBER:
                ret += parser.getNumberChars().length();
                break;
              case JSONParser.NUMBER:
                ret += parser.getDouble();
                break;
              case JSONParser.LONG:
                ret += parser.getLong();
                break;
              default:
                ret += ev;
            }
          }

          if (ev == JSONParser.EOF) break;
        }
      } catch (IOException ex) {
        // shouldn't happen
        log.error(String.valueOf(ret)); // use ret
      } catch (JSONParser.ParseException ex) {
        // OK
      } catch (Throwable ex) {
        log.error(lastParser(), ex);
        throw new RuntimeException(ex);
      }
    }
  }

  public static class Num {
    public String digits;

    public Num(String digits) {
      this.digits = digits;
    }

    @Override
    public String toString() {
      return new String("NUMBERSTRING(" + digits + ")");
    }

    @Override
    public boolean equals(Object o) {
      return (o instanceof Num && digits.equals(((Num) o).digits));
    }

    @Override
    public int hashCode() {
      return digits.hashCode();
    }
  }

  public static class BigNum extends Num {
    @Override
    public String toString() {
      return new String("BIGNUM(" + digits + ")");
    }

    public BigNum(String digits) {
      super(digits);
    }
  }

  // Oh, what I wouldn't give for Java5 varargs and autoboxing
  public static Long o(int l) {
    return (long) l;
  }

  public static Long o(long l) {
    return l;
  }

  public static Double o(double d) {
    return d;
  }

  public static Boolean o(boolean b) {
    return b;
  }

  public static Num n(String digits) {
    return new Num(digits);
  }

  public static Num bn(String digits) {
    return new BigNum(digits);
  }

  public static Object t = Boolean.TRUE;
  public static Object f = Boolean.FALSE;
  public static Object a =
      new Object() {
        @Override
        public String toString() {
          return "ARRAY_START";
        }
      };
  public static Object A =
      new Object() {
        @Override
        public String toString() {
          return "ARRAY_END";
        }
      };
  public static Object m =
      new Object() {
        @Override
        public String toString() {
          return "OBJECT_START";
        }
      };
  public static Object M =
      new Object() {
        @Override
        public String toString() {
          return "OBJECT_END";
        }
      };
  public static Object N =
      new Object() {
        @Override
        public String toString() {
          return "NULL";
        }
      };
  public static Object e =
      new Object() {
        @Override
        public String toString() {
          return "EOF";
        }
      };

  // match parser states with the expected states
  public static void parse(JSONParser p, String input, Object[] expected) throws IOException {
    for (int i = 0; i < expected.length; i++) {
      int ev = p.nextEvent();
      Object exp = expected[i];
      Object got = null;

      switch (ev) {
        case JSONParser.ARRAY_START:
          got = a;
          break;
        case JSONParser.ARRAY_END:
          got = A;
          break;
        case JSONParser.OBJECT_START:
          got = m;
          break;
        case JSONParser.OBJECT_END:
          got = M;
          break;
        case JSONParser.LONG:
          got = o(p.getLong());
          break;
        case JSONParser.NUMBER:
          if (exp instanceof Double) {
            got = o(p.getDouble());
          } else {
            got = n(p.getNumberChars().toString());
          }
          break;
        case JSONParser.BIGNUMBER:
          got = bn(p.getNumberChars().toString());
          break;
        case JSONParser.NULL:
          got = N;
          p.getNull();
          break; // optional
        case JSONParser.BOOLEAN:
          got = o(p.getBoolean());
          break;
        case JSONParser.EOF:
          got = e;
          break;
        case JSONParser.STRING:
          got = p.getString();
          break;
        default:
          got = "Unexpected Event Number " + ev;
      }

      if (!(exp == got || exp.equals(got))) {
        fail(
            "Fail: String='"
                + input
                + "'"
                + "\n\tINPUT="
                + got
                + "\n\tEXPECTED="
                + exp
                + "\n\tAT RULE "
                + i);
      }
    }
  }

  public static void parse(String input, Object[] expected) throws IOException {
    parse(input, (flags & JSONParser.ALLOW_SINGLE_QUOTES) == 0 || random().nextBoolean(), expected);
  }

  public static void parse(String input, boolean changeSingleQuote, Object[] expected)
      throws IOException {
    String in = input;
    if (changeSingleQuote) {
      in = in.replace('\'', '"');
    }
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
      JSONParser p = getParser(in, i, -1);
      if (p == null) break;
      parse(p, in, expected);
    }
  }

  public static void err(String input) throws IOException {
    try {
      JSONParser p = getParser(input);
      while (p.nextEvent() != JSONParser.EOF) {}
    } catch (Exception e) {
      return;
    }
    fail("Input should failed:'" + input + "'");
  }

  @Test
  public void testNull() throws IOException {
    flags = JSONParser.FLAGS_STRICT;
    err("nul");
    err("n");
    err("nullz");
    err("[nullz]");
    flags = JSONParser.FLAGS_DEFAULT;

    parse("[null]", "[0]");
    parse("{'hi':null}", new Object[] {m, "hi", N, M, e});
  }

  @Test
  public void testBool() throws IOException {
    flags = JSONParser.FLAGS_STRICT;
    err("[True]");
    err("[False]");
    err("[TRUE]");
    err("[FALSE]");
    err("[truex]");
    err("[falsex]");
    err("[tru]");
    err("[fals]");
    err("[tru");
    err("[fals");
    err("t");
    err("f");
    flags = JSONParser.FLAGS_DEFAULT;

    parse("[false,true, false , true ]", new Object[] {a, f, t, f, t, A, e});
  }

  @Test
  public void testString() throws IOException {
    // NOTE: single quotes are converted to double quotes by this
    // testsuite!
    err("[']");
    err("[',]");
    err("{'}");
    err("{',}");

    err("['\\u111']");
    err("['\\u11']");
    err("['\\u1']");
    err("['\\']");

    flags = JSONParser.FLAGS_STRICT;
    err("['\\ ']"); // escape of non-special char
    err("['\\U1111']"); // escape of non-special char
    flags = JSONParser.FLAGS_DEFAULT;

    parse("['\\ ']", new Object[] {a, " ", A, e}); // escape of non-special char
    parse("['\\U1111']", new Object[] {a, "U1111", A, e}); // escape of non-special char

    parse("['']", new Object[] {a, "", A, e});
    parse("['\\\\']", new Object[] {a, "\\", A, e});
    parse("['X\\\\']", new Object[] {a, "X\\", A, e});
    parse("['\\\\X']", new Object[] {a, "\\X", A, e});
    parse("[\"\\\"\"]", new Object[] {a, "\"", A, e});

    parse("['\\'']", true, new Object[] {a, "\"", A, e});
    parse("['\\'']", false, new Object[] {a, "'", A, e});

    String esc = "\\n\\r\\tX\\b\\f\\/\\\\X\\\"";
    String exp = "\n\r\tX\b\f/\\X\"";
    parse("['" + esc + "']", new Object[] {a, exp, A, e});
    parse(
        "['" + esc + esc + esc + esc + esc + "']",
        new Object[] {a, exp + exp + exp + exp + exp, A, e});

    esc = "\\u004A";
    exp = "\u004A";
    parse("['" + esc + "']", new Object[] {a, exp, A, e});

    esc = "\\u0000\\u1111\\u2222\\u12AF\\u12BC\\u19DE";
    exp = "\u0000\u1111\u2222\u12AF\u12BC\u19DE";
    parse("['" + esc + "']", new Object[] {a, exp, A, e});
  }

  @Test
  public void testNumbers() throws IOException {
    flags = JSONParser.FLAGS_STRICT;

    err("[00]");
    err("[003]");
    err("[00.3]");
    err("[1e1.1]");
    err("[+1]");
    err("[NaN]");
    err("[Infinity]");
    err("[--1]");

    flags = JSONParser.FLAGS_DEFAULT;

    String lmin = "-9223372036854775808";
    String lminNot = "-9223372036854775809";
    String lmax = "9223372036854775807";
    String lmaxNot = "9223372036854775808";

    String bignum = "12345678987654321357975312468642099775533112244668800152637485960987654321";

    parse("[0,1,-1,543,-876]", new Object[] {a, o(0), o(1), o(-1), o(543), o(-876), A, e});
    parse("[-0]", new Object[] {a, o(0), A, e});

    parse(
        "[" + lmin + "," + lmax + "]",
        new Object[] {a, o(Long.MIN_VALUE), o(Long.MAX_VALUE), A, e});

    parse("[" + bignum + "]", new Object[] {a, bn(bignum), A, e});
    parse("[" + "-" + bignum + "]", new Object[] {a, bn("-" + bignum), A, e});

    parse("[" + lminNot + "]", new Object[] {a, bn(lminNot), A, e});
    parse("[" + lmaxNot + "]", new Object[] {a, bn(lmaxNot), A, e});

    parse("[" + lminNot + "," + lmaxNot + "]", new Object[] {a, bn(lminNot), bn(lmaxNot), A, e});

    // bignum many digits on either side of decimal
    String t = bignum + "." + bignum;
    parse("[" + t + "," + "-" + t + "]", new Object[] {a, bn(t), bn("-" + t), A, e});
    err("[" + t + ".1" + "]"); // extra decimal
    err("[" + "-" + t + ".1" + "]");

    // bignum exponent w/o fraction
    t = "1" + "e+" + bignum;
    parse("[" + t + "," + "-" + t + "]", new Object[] {a, bn(t), bn("-" + t), A, e});
    t = "1" + "E+" + bignum;
    parse("[" + t + "," + "-" + t + "]", new Object[] {a, bn(t), bn("-" + t), A, e});
    t = "1" + "e" + bignum;
    parse("[" + t + "," + "-" + t + "]", new Object[] {a, bn(t), bn("-" + t), A, e});
    t = "1" + "E" + bignum;
    parse("[" + t + "," + "-" + t + "]", new Object[] {a, bn(t), bn("-" + t), A, e});
    t = "1" + "e-" + bignum;
    parse("[" + t + "," + "-" + t + "]", new Object[] {a, bn(t), bn("-" + t), A, e});
    t = "1" + "E-" + bignum;
    parse("[" + t + "," + "-" + t + "]", new Object[] {a, bn(t), bn("-" + t), A, e});

    t = bignum + "e+" + bignum;
    parse("[" + t + "," + "-" + t + "]", new Object[] {a, bn(t), bn("-" + t), A, e});
    t = bignum + "E-" + bignum;
    parse("[" + t + "," + "-" + t + "]", new Object[] {a, bn(t), bn("-" + t), A, e});
    t = bignum + "e" + bignum;
    parse("[" + t + "," + "-" + t + "]", new Object[] {a, bn(t), bn("-" + t), A, e});

    t = bignum + "." + bignum + "e" + bignum;
    parse("[" + t + "," + "-" + t + "]", new Object[] {a, bn(t), bn("-" + t), A, e});

    err("[1E]");
    err("[1E-]");
    err("[1E+]");
    err("[1E+.3]");
    err("[1E+0.3]");
    err("[1E+1e+3]");
    err("[" + bignum + "e" + "]");
    err("[" + bignum + "e-" + "]");
    err("[" + bignum + "e+" + "]");
    err("[" + bignum + "." + bignum + "." + bignum + "]");

    double[] vals =
        new double[] {
          0,
          0.1,
          1.1,
          Double.MAX_VALUE,
          Double.MIN_VALUE,
          2.2250738585072014E-308, /* Double.MIN_NORMAL */
        };
    for (int i = 0; i < vals.length; i++) {
      double d = vals[i];
      parse("[" + d + "," + -d + "]", new Object[] {a, o(d), o(-d), A, e});
    }

    // MIN_NORMAL has the max number of digits (23), so check that
    // adding an extra digit causes BIGNUM to be returned.
    t = "2.2250738585072014E-308" + "0";
    parse("[" + t + "," + "-" + t + "]", new Object[] {a, bn(t), bn("-" + t), A, e});
    // check it works with a leading zero too
    t = "0.2250738585072014E-308" + "0";
    parse("[" + t + "," + "-" + t + "]", new Object[] {a, bn(t), bn("-" + t), A, e});

    // check that overflow detection is working properly w/ numbers that don't cause a wrap to
    // negatives when multiplied by 10
    t = "1910151821265210155" + "0";
    parse("[" + t + "," + "-" + t + "]", new Object[] {a, bn(t), bn("-" + t), A, e});

    for (int i = 0; i < 1000000; i++) {
      long val = random().nextLong();
      String sval = Long.toString(val);
      JSONParser parser = getParser("[" + val + "]");
      parser.nextEvent();
      assertEquals(JSONParser.LONG, parser.nextEvent());
      if (random().nextBoolean()) {
        assertEquals(val, parser.getLong());
      } else {
        CharArr chars = parser.getNumberChars();
        assertEquals(sval, chars.toString());
      }
    }
  }

  @Test
  public void testArray() throws IOException {
    parse("[]", "[]");
    parse("[ ]", "[]");
    parse(" \r\n\t[\r\t\n ]\r\n\t ", "[]");

    parse("[0]", "[l]");
    parse("['0']", "[s]");
    parse("[0,'0',0.1]", "[lsn]");

    parse("[[[[[]]]]]", "[[[[[]]]]]");
    parse("[[[[[0]]]]]", "[[[[[l]]]]]");

    err("]");
    err("[");
    err("[[]");
    err("[]]");
    err("[}");
    err("{]");
    err("['a':'b']");

    flags = JSONParser.FLAGS_STRICT;
    err("[,]"); // test that extra commas fail
    err("[[],]");
    err("['a',]");
    err("['a',]");
    flags = JSONParser.FLAGS_DEFAULT;

    parse("[,]", "[]"); // test extra commas
    parse("[,,]", "[]");
    parse("[,,,]", "[]");
    parse("[[],]", "[[]]");
    parse("[[,],]", "[[]]");
    parse("[[,,],,]", "[[]]");
    parse("[,[,,],,]", "[[]]");
    parse("[,5,[,,5],,]", "[l[l]]");
  }

  @Test
  public void testObject() throws IOException {
    parse("{}", "{}");
    parse("{}", "{}");
    parse(" \r\n\t{\r\t\n }\r\n\t ", "{}");

    parse("{'':null}", "{s0}");

    err("}");
    err("[}]");
    err("{");
    err("[{]");
    err("{{}");
    err("[{{}]");
    err("{}}");
    err("[{}}]");
    err("{1}");
    err("[{1}]");
    err("{'a'}");
    err("{'a','b'}");
    err("{[]:'b'}");
    err("{{'a':'b'}:'c'}");
    err("{'a','b'}}");

    // bare strings allow these to pass
    flags = JSONParser.FLAGS_STRICT;
    err("{null:'b'}");
    err("{true:'b'}");
    err("{false:'b'}");
    err("{,}"); // test that extra commas fail
    err("{{},}");
    err("{'a':'b',}");
    flags = JSONParser.FLAGS_DEFAULT;

    parse("{}", new Object[] {m, M, e});
    parse("{,}", new Object[] {m, M, e});
    parse("{,,}", new Object[] {m, M, e});
    parse("{'a':{},}", new Object[] {m, "a", m, M, M, e});
    parse("{'a':{},,}", new Object[] {m, "a", m, M, M, e});
    parse("{,'a':{,},,}", new Object[] {m, "a", m, M, M, e});
    parse("{'a':'b'}", new Object[] {m, "a", "b", M, e});
    parse("{'a':5}", new Object[] {m, "a", o(5), M, e});
    parse("{'a':null}", new Object[] {m, "a", N, M, e});
    parse("{'a':[]}", new Object[] {m, "a", a, A, M, e});
    parse("{'a':{'b':'c'}}", new Object[] {m, "a", m, "b", "c", M, M, e});

    String big = "Now is the time for all good men to come to the aid of their country!";
    String bigger = big + big + big + big + big;
    parse(
        "{'" + bigger + "':'" + bigger + "','a':'b'}",
        new Object[] {m, bigger, bigger, "a", "b", M, e});

    flags = JSONParser.ALLOW_UNQUOTED_KEYS;
    parse("{a:'b'}", new Object[] {m, "a", "b", M, e});
    parse("{null:'b'}", new Object[] {m, "null", "b", M, e});
    parse("{true: 'b'}", new Object[] {m, "true", "b", M, e});
    parse("{ false :'b'}", new Object[] {m, "false", "b", M, e});
    parse(
        "{null:null, true : true , false : false , x:'y',a:'b'}",
        new Object[] {m, "null", N, "true", t, "false", f, "x", "y", "a", "b", M, e});
    flags = JSONParser.FLAGS_DEFAULT | JSONParser.ALLOW_MISSING_COLON_COMMA_BEFORE_OBJECT;
    parse("{'a'{'b':'c'}}", new Object[] {m, "a", m, "b", "c", M, M, e});
    parse(
        "{'a': [{'b':'c'} {'b':'c'}]}",
        new Object[] {m, "a", a, m, "b", "c", M, m, "b", "c", M, A, M, e});
    parse(
        "{'a' [{'b':'c'} {'b':'c'}]}",
        new Object[] {m, "a", a, m, "b", "c", M, m, "b", "c", M, A, M, e});
    parse("{'a':[['b']['c']]}", new Object[] {m, "a", a, a, "b", A, a, "c", A, A, M, e});
    parse(
        "{'a': {'b':'c'} 'd': {'e':'f'}}",
        new Object[] {m, "a", m, "b", "c", M, "d", m, "e", "f", M, M, e});
    parse(
        "{'a': {'b':'c'} d: {'e':'f'}}",
        new Object[] {m, "a", m, "b", "c", M, "d", m, "e", "f", M, M, e});

    flags =
        JSONParser.FLAGS_DEFAULT
            | JSONParser.ALLOW_MISSING_COLON_COMMA_BEFORE_OBJECT
            | JSONParser.OPTIONAL_OUTER_BRACES;
    parse("'a':{'b':'c'}", new Object[] {m, "a", m, "b", "c", M, M, e});
    parse("'a':{'b':'c'}", true, new Object[] {m, "a", m, "b", "c", M, M, e});
    parse("a:'b'", new Object[] {m, "a", "b", M, e});

    flags = JSONParser.FLAGS_DEFAULT;
  }

  @Test
  public void testBareString() throws Exception {
    flags =
        JSONParser.ALLOW_UNQUOTED_STRING_VALUES | JSONParser.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER;
    String[] strings =
        new String[] {
          "t",
          "f",
          "n",
          "a",
          "tru",
          "fals",
          "nul",
          "abc",
          "trueX",
          "falseXY",
          "nullZ",
          "truetrue",
          "$true",
          "a.bc.def",
          "a_b-c/d"
        };

    for (String s : strings) {
      parse(s, new Object[] {s, e});
      parse("[" + s + "]", new Object[] {a, s, A, e});
      parse("[ " + s + ", " + s + " ]", new Object[] {a, s, s, A, e});
      parse("[" + s + "," + s + "]", new Object[] {a, s, s, A, e});
      parse(
          "\u00a0[\u00a0\r\n\t\u00a0" + s + "\u00a0,\u00a0\u00a0" + s + "\u00a0]\u00a0",
          new Object[] {a, s, s, A, e});
    }

    flags |= JSONParser.ALLOW_UNQUOTED_KEYS;
    for (String s : strings) {
      parse("{" + s + ":" + s + "}", new Object[] {m, s, s, M, e});
      parse("{ " + s + " \t\n\r:\t\n\r " + s + "\t\n\r}", new Object[] {m, s, s, M, e});
    }

    parse(
        "{true:true, false:false, null:null}",
        new Object[] {m, "true", t, "false", f, "null", N, M, e});

    flags = JSONParser.FLAGS_DEFAULT;
  }

  @Test
  public void testAPI() throws IOException {
    JSONParser p = new JSONParser("[1,2]");
    assertEquals(JSONParser.ARRAY_START, p.nextEvent());
    // no nextEvent for the next objects...
    assertEquals(1, p.getLong());
    assertEquals(2, p.getLong());
  }

  @Test
  public void testComments() throws IOException {
    parse(
        "#pre comment\n{//before a\n  'a' /* after a **/ #before separator\n  : /* after separator */ {/*before b*/'b'#after b\n://before c\n'c'/*after c*/}/*after close*/}#post comment no EOL",
        new Object[] {m, "a", m, "b", "c", M, M, e});
  }

  // rfc7159 permits any JSON values to be top level values
  @Test
  public void testTopLevelValues() throws Exception {
    parse("\"\"", new Object[] {""});
    parse("\"hello\"", new Object[] {"hello"});
    parse("true", new Object[] {t});
    parse("false", new Object[] {f});
    parse("null", new Object[] {N});
    parse("42", new Object[] {42L});
    parse("1.414", new Object[] {1.414d});
    parse("/*comment*/1.414/*more comment*/", new Object[] {1.414d});
  }
}
